# coding=utf-8
from flask import current_app
from utils.views import bp_cache
from datetime import datetime
from urllib.parse import urlparse
from docx import Document
from docx.shared import Pt
from docx.enum.text import WD_ALIGN_PARAGRAPH
from typing import List, Dict
import time
import random
import configparser
import sys
import asyncio
import aiohttp
import requests
import os
import fitz  # PyMuPDF
import re
import html


"""
读取配置文件
"""
try:
    cf = configparser.ConfigParser()
    # cf.optionsxform = str  # 若出现不分大小写字符的情况 用这个
    cf.read('./conf/config.ini')
    aikey = cf.get('ai','key')
    apikey = cf.get('api','apikey')
    jainum = cf.get('web','jai_num')
except Exception as e:
    current_app.logger.debug('请确保config.ini是否存在,或配置不正确！')
    current_app.logger.debug(e)
    sys.exit(0)

class aiModel(object):

    # 处理论文，整理成列表格式
    def handle_text(self,items,db):
        data = []
        for item in items:
            if bp_cache.get('process_id_' + str(item['p_id'])):
                continue
            current_app.logger.debug(f"================用户id:{item['user_id']} 主流程id:{item['id']} 子流程id:{item['p_id']} 类型:{item['type']} 处理论文开始=================")
            # 降AI类型
            type = item['type']
            # 查询查重接口返回
            reportApiRes = self.getReportApi(item['submit_id'])
            # 接口错误或文件有问题，跳过
            if 'data' not in reportApiRes:
                current_app.logger.debug("获取报告接口异常，跳过")
                continue
            # 查重未完成，跳过
            if not reportApiRes['data']['AI']:
                current_app.logger.debug("AI报告还未生成")
                continue
            # ai率
            ai = reportApiRes['data']['AI'].strip()
            # 相似率
            similarity = reportApiRes['data']['similarity'].strip()
            # AI报告
            ai_report = reportApiRes['data']['AIReport'].strip()
            # 查重报告
            plagiarism_report = reportApiRes['data']['plagiarismReport'].strip()
            # 回收状态
            recycle_status = 0
            if item['num'] >= int(jainum) and ai != '0%' and ai != '*%':
                recycle_status = 1
            if 'Unable to detect' in ai:
                recycle_status = 1
            # 查不出AI/流程字数超10次/AI达标，流程改为完成
            if item['num'] >= int(jainum) or 'Unable to detect' in ai or ai == '0%' or (ai == '*%' and type == 0):
                db.execute_many_sqls([
                    ("UPDATE fa_task_jai_process SET status = 1,updatetime = %s,ai = %s,similarity = %s, plagiarism_report = %s,ai_report = %s WHERE id = %s", 
                    (time.time(),ai,similarity,plagiarism_report,ai_report,item['p_id'])),
                    ("UPDATE fa_task_jai SET status = 1,updatetime = %s,recycle_status = %s WHERE id = %s",
                    (time.time(),recycle_status,item['id']))
                ])
                current_app.logger.debug(f"================用户id:{item['user_id']} 主流程id:{item['id']} 子流程id:{item['p_id']} AI率:{ai} 文章已经降到标准 流程完成=================")
                continue
            db.execute("UPDATE fa_task_jai_process SET updatetime = %s,ai = %s,similarity = %s, plagiarism_report = %s,ai_report = %s WHERE id = %s",
                       (time.time(),ai,similarity,plagiarism_report,ai_report, item['p_id']))
            # 开始处理文本
            # 下载pdf到本地
            downloadRes = self.download_file(ai_report)
            current_app.logger.debug("下载ai报告结果")
            current_app.logger.debug(downloadRes)
            if not downloadRes:
                current_app.logger.debug("下载pdf异常")
                continue
            # 下载下来的pdf路径
            filepath = downloadRes['filepath']
            # 根据ai报告和文本判断哪些段落需要降AI
            # 如果是*号，获取`降AI段落`字段，如果是空全文降，反之按照段落
            ai_para = item['ai_para']
            if not ai_para:
                ai_para = ()
            else:
                ai_para = tuple(map(int, ai_para.split(',')))
            textRes = self.mark_highlighted_paragraphs(item['id'],item['p_id'],item['user_id'],ai_para,item['content'],filepath)
            data.append(textRes)
            current_app.logger.debug(f"================用户id:{item['user_id']} 主流程id:{item['id']} 子流程id:{item['p_id']} 处理论文结束=================")
            bp_cache.set('process_id_' + str(item['p_id']),str(item['p_id']),timeout=300)
        return data
    
    # """
    # 标记原始文章中哪些段落被高亮
    
    # :param original_text: 原始文章文本
    # :param pdf_path: PDF文件路径
    # :return: [{
    #     'paragraph': str,        # 段落内容
    #     'is_highlighted': bool,  # 是否包含高亮
    #     'index': int            # 原始位置索引
    # }]
    # """
    def mark_highlighted_paragraphs(self,id,pid,userid,ai_para,original_text: str, pdf_path: str) -> List[Dict]:
        # 提取PDF中所有高亮文本片段
        highlight_fragments = self.extract_highlight_fragments(pdf_path)
        
        # 拆分原始文章为段落
        paragraphs = self.split_paragraphs(original_text)
        # 标记哪些段落包含高亮片段
        marked_paragraphs = [
            {
                'paragraph': para,
                'is_highlighted': self.is_paragraph_highlighted(para, highlight_fragments),
                'index': idx + 1
            }
            for idx, para in enumerate(paragraphs)
        ]
        # 获取所有包含高亮的段落
        highlighted_indices = tuple(
            idx + 1 for idx, p in enumerate(marked_paragraphs)
            if p['is_highlighted']
        )
        for i in marked_paragraphs:
            i['ai_para'] = highlighted_indices
            i['id'] = id
            i['p_id'] = pid
            i['user_id'] = userid
            # 上次流程的高亮段落和本次流程高亮段落都为空，全部标记为高亮
            if not ai_para and not highlighted_indices:
                i['is_highlighted'] = True
            # 本次流程高亮段落为空，就按照上次流程的高亮段落
            if ai_para and not highlighted_indices and i['index'] in ai_para :
                i['is_highlighted'] = True
                i['ai_para'] = ai_para
            if ai_para and not highlighted_indices:
                i['ai_para'] = ai_para
        return marked_paragraphs

    # """提取PDF中所有高亮文本片段"""
    def extract_highlight_fragments(self,pdf_path: str) -> List[str]:
        doc = fitz.open(pdf_path)
        highlights = []
        
        for page in doc:
            # 获取本页所有背景矩形
            bg_rects = [
                fitz.Rect(d['rect'])
                for d in page.get_drawings() 
                if d.get('fill') and not d.get('stroke')  # 关键判断：有填充无描边
            ]
            
            # 检查每个文本块是否与背景矩形相交
            for block in page.get_text("blocks"):
                x0, y0, x1, y1, text = block[:5]
                if not text.strip():
                    continue
                    
                block_rect = fitz.Rect(x0, y0, x1, y1)
                if any(bg_rect.intersects(block_rect) for bg_rect in bg_rects):
                    highlights.append(text.strip())
        
        doc.close()
        return highlights

    # """按空行拆分文本为段落"""
    def split_paragraphs(self,text: str) -> List[str]:
        return [p.strip() for p in re.split(r'\n\s*\n', text) if p.strip()]

    # """判断段落是否包含任何高亮片段"""
    def is_paragraph_highlighted(self,paragraph: str, fragments: List[str]) -> bool:
        para_clean = self.normalize_text(paragraph)
        return any(
            self.normalize_text(fragment) in para_clean 
            for fragment in fragments
        )

    # """标准化文本用于比较（移除空格/换行/大小写）"""
    def normalize_text(self,text: str) -> str:
        return html.unescape(re.sub(r'\s+', '', text).lower())

    # """
    # 下载在线文件到本地，按日期分类存储，覆盖已存在文件
    
    # :param url: 要下载的文件URL
    # :param custom_filename: 可选的自定义文件名
    # :return: 字典 {'success': bool, 'message': str, 'filepath': str, 'filename': str}
    # """
    def download_file(self,url):
        try:
            # 创建以日期命名的下载目录
            today = datetime.now().strftime("%Y-%m-%d")
            download_dir = os.path.join("static", "download", today)
            os.makedirs(download_dir, exist_ok=True)
            # 获取文件流
            response = requests.get(url, stream=True, allow_redirects=True)
            response.raise_for_status()
            # 从URL中提取文件名
            filename = os.path.basename(url)
            # 构建保存路径
            save_path = os.path.join(download_dir, filename)
            # 保存文件（直接覆盖已存在文件）
            with open(save_path, 'wb') as f:
                for chunk in response.iter_content(8192):
                    if chunk:
                        f.write(chunk)
            return {
                'success': True,
                'message': '文件下载成功',
                'filepath': save_path,
                'filename': filename,
                'download_dir': download_dir
            }

        except Exception as e:
            current_app.logger.error(f"ai报告下载异常: {e}")
            return None
        
    # 根据文本创建docx文件
    def create_word_document(self,text):
        # 创建以日期命名的下载目录
        today = datetime.now().strftime("%Y-%m-%d")
        file_dir = os.path.join("static", "docx", today)
        os.makedirs(file_dir, exist_ok=True)
        try:
            """将文本按段落生成Word文档"""
            doc = Document()
            # 设置正文样式
            style = doc.styles['Normal']
            font = style.font
            font.name = 'Times New Roman'
            font.size = Pt(11)
            
            # 分割段落（按两个换行符分割）
            paragraphs = [p.replace('\n',' ').strip() for p in text.split('\n\n') if p.strip()]
            
            # 添加所有段落
            for para in paragraphs:
                p = doc.add_paragraph(para)
                p.alignment = WD_ALIGN_PARAGRAPH.LEFT  # 左对齐
            # 获取当前时间戳（毫秒级）
            timestamp_ms = int(time.time() * 1000)
            # 生成8位随机数（可能以0开头）
            random_num = f"{random.randint(0, 99999999):08d}"
            # 组合文件名
            filename = f"{timestamp_ms}_{random_num}.docx"
            # 构建保存路径
            save_path = os.path.join(file_dir, filename)
            # 保存文档
            doc.save(save_path)
            return {
                    'success': True,
                    'message': '文件下载成功',
                    'filepath': save_path,
                    'filename': filename,
                    'file_dir': file_dir
                }
        except Exception as e:
            current_app.logger.error(f"生成docx文件异常: {e}")
            return None
        

    # """按index排序合并段落，生成新文章"""
    def transform_data(self,original_data):
        sorted_items = sorted(original_data, key=lambda x: x['index'])
        return {
            'content': '\n\n\n\n'.join(item['paragraph'] for item in sorted_items),
            'ai_para': original_data[0]['ai_para'],  # 取第一个元素的元组
            'id': original_data[0]['id'],
            'p_id': original_data[0]['p_id'],
            'user_id': original_data[0]['user_id']
        }

    # """
    # 异步获取第三方API数据
    # :param session: aiohttp session
    # :param url: API地址
    # :param item_id: 项目ID
    # :param timeout: 超时时间(秒)
    # :return: 获取的数据或错误信息
    # """
    async def fetch_ai_handle(self,session,item):
        current_app.logger.debug(f"================用户id:{item['user_id']} 主流程id:{item['id']} 子流程id:{item['p_id']} 段落:{item['index']} AI降重开始=================")
        """异步调用API"""
        text = item['paragraph']
        item_id = item['id']
        if item['is_highlighted']:
            url = "https://api.deepseek.com/v1/chat/completions"
            headers = {
                "Authorization": "Bearer " + aikey,
                "Content-Type": "application/json"
            }
            payload = {
                "model": "deepseek-chat",
                "messages": [
                    {
                        "content": """
# Role: 文本重构大师

## Profile
- language: 英文
- description: 专业从事英文文本重构与优化，擅长简化复杂句式、调整语言风格，同时保持原文核心信息完整
- background: 拥有语言学与写作教学背景，长期从事文本优化与跨语言表达工作
- personality: 严谨细致，注重细节，善于平衡语言规范与表达效果
- expertise: 句式简化、词汇替换、语法调整、风格转换
- target_audience: 需要英文文本优化服务的各类用户

## Skills
1. 句式重构技能
   - 长句拆分: 适当将过于复杂的长句拆分为简洁短句。但需保留必要的长句用于解释关键概念。
   - 结构简化: 减少从句嵌套，保持逻辑清晰
   - 语态转换: 适当减少句子中的被动语态，增加主动语态
   - 连接优化: 避免句子衔接过于生硬，降低连接词使用密度，例如：and, but, which, where等等
2. 词汇处理技能
   - 同义词更换: 在不影响原始表达的情况下，适当使用困惑度更高的近义词更换单词 
   - 表达中性化: 适当将正向强化词转换为中性表达
   - 修饰添加: 适当增加不影响语义的修饰成分
   - 错误引入: 谨慎添加不影响理解的语法错误

## Rules
1. 文本处理原则：
   - 保持原意: 所有修改不得改变原文核心含义
   - 适度简化: 在可读性与准确性间保持平衡
   - 风格统一: 确保整体语言风格协调一致
   - 量化控制: 严格遵循字数与句子数量要求
2. 语言规范准则：
   - 语法适度: 仅添加不影响理解的轻微语法错误
   - 逻辑连贯: 确保句子间逻辑关系清晰明确
   - 表达直接: 使用简单直接的表达方式
3. 输出限制条件：
   - 格式保持1: 保留原文段落结构和格式
   - 格式保持2: 不得更改原文段落标号，如段落开头的1.1、1.2等
   - 学术规范: 遵循学术写作的规范，不得更改包括引用、注释和参考文献的格式。
   - 内容限制: 不添加新的分点描述和特殊格式
   - 长度控制: 确保单词数不低于原文的95%并不超过原文的120%
   - 句子数量: 重构后句子数不低于原文的95%并不超过原段落的120%

## Workflows

- 目标: 完成符合所有要求的英文文本重构
- 步骤 1: 分析原文结构，识别复杂句式和词汇
- 步骤 2: 执行句式简化，拆分长句，调整语态
- 步骤 3: 替换词汇，调整表达方式，控制连接词
- 步骤 4: 添加适度语法错误
- 步骤 5: 检查长度要求，确保符合所有规范
- 预期结果: 产出符合所有技术要求的重构文本
- 注意: 你只需要输出修改后的文本，不需要其他任何前提说明和总结性文本。

## Initialization
作为文本重构大师，你必须遵守上述Rules，按照Workflows执行任务。
                            """,
                        "role": "system"
                    },
                    {
                        "content": text,
                        "role": "user"
                    }
                ],
                "temperature": 1.5
            }
            try:
                async with session.post(url, json=payload, headers=headers) as response:
                    if response.status == 200:
                        data = await response.json()
                        content = data['choices'][0]['message']['content']
                        # 把显式的 \n 替换成实际换行
                        content = content.replace('\\n', '\n')
                        # 将所有换行符替换为一个空格（包括段落间的多行换行）
                        content = content.replace('\n', ' ')
                        # 替换多个空格为一个
                        content = ' '.join(content.split())
                        item['paragraph'] = content
                        current_app.logger.debug(f"================用户id:{item['user_id']} 主流程id:{item['id']} 子流程id:{item['p_id']} 段落:{item['index']} AI降重结束 =================")
                        return item
                    else:
                        error_msg = f"AI接口: API request failed for item {item_id}, status: {response.status}"
                        current_app.logger.error(error_msg)
                        return {"error": error_msg, "item_id": item_id}
            except asyncio.TimeoutError:
                error_msg = f"AI接口: Timeout for item {item_id}"
                current_app.logger.error(error_msg)
                return {"error": error_msg, "item_id": item_id}
            except Exception as e:
                error_msg = f"AI接口: Error for item {item_id}: {str(e)}"
                current_app.logger.error(error_msg)
                return {"error": error_msg, "item_id": item_id}
        current_app.logger.debug(f"================用户id:{item['user_id']} 主流程id:{item['id']} 子流程id:{item['p_id']} 段落:{item['index']} AI降重结束(无需降重) =================")
        return item
        
    # 创建异步任务
    async def batch_handle_subprocess(self, session, items):
        tasks = [self.fetch_ai_handle(session, item) for item in items]
        return await asyncio.gather(*tasks)

    # 异步处理主流程
    async def batch_handle_main_process(self,items):
        # 设置TCP连接器，限制并发连接数
        # connector = aiohttp.TCPConnector(limit=30)
        # async with aiohttp.ClientSession(connector=connector) as session:
        async with aiohttp.ClientSession() as session:
            tasks = [
                self.batch_handle_subprocess(session, item)
                for item in items
            ]
            return await asyncio.gather(*tasks)
        
    # 查重服务器上传文件接口
    def uploadApi(self,file_path):
        url = "https://api.turnitin.club/api/userccm/essauraUpload"
        # url = "http://fastadmin.test/api/userccm/essauraUpload"
        data = {
            "apikey": apikey,
        }

        # 检查文件是否存在
        if not os.path.exists(file_path):
            current_app.logger.error(f"文件 {file_path} 不存在")
            return {'error':f"文件 {file_path} 不存在"}
        try:
            # 使用 with 自动管理文件
            with open(file_path, "rb") as file:
                files = {
                    "file": (os.path.basename(file_path), file, "application/vnd.openxmlformats-officedocument.wordprocessingml.document")
                }
                
                # 发送请求，设置超时
                response = requests.post(url, data=data, files=files, timeout=30)
                response.raise_for_status()  # 检查 HTTP 错误（4xx/5xx）
                
                result = response.json()
                current_app.logger.debug("uploadApi 返回数据:")
                current_app.logger.debug(result)
                # 校验返回数据结构
                if not isinstance(result, dict):
                    current_app.logger.error("uploadApi 接口返回数据格式错误")
                    return {'error':"接口返回数据格式错误"}
                if result.get("code", 0) != 1:  # 假设 code=0 表示成功
                    current_app.logger.error(f"uploadApi 上传失败: {result.get('msg', '未知错误')}")
                    return {'error':f"上传失败: {result.get('msg', '未知错误')}"}
                else:
                    current_app.logger.info(f"uploadApi 上传成功! 返回数据ID: {result['data']}")
                    return {'data':result['data']}

        except requests.exceptions.RequestException as e:
            current_app.logger.error(f"uploadApi 网络请求失败: {e}")
            return {'error':f"网络请求失败: {e}"}
        except ValueError as e:
            current_app.logger.error(f"uploadApi 数据解析失败: {e}")
            return {'error':f"数据解析失败: {e}"}
        except Exception as e:
            current_app.logger.error(f"uploadApi 未知错误: {e}")
            return {'error':f"未知错误: {e}"}

    # 查重服务器查询报告接口 
    def getReportApi(self,id):
        url = "https://api.turnitin.club/api/userccm/getReport"
        # url = "http://fastadmin.test/api/userccm/getReport"
        data = {
            "apikey": apikey,
            "data_id": id,
        }
        try:
            response = requests.post(url, data=data, timeout=30)
            response.raise_for_status()  # 检查 HTTP 错误（4xx/5xx）
            
            result = response.json()
            current_app.logger.debug("getReportApi 返回数据:")
            current_app.logger.debug(result)
            # 校验返回数据结构
            if not isinstance(result, dict):
                current_app.logger.error("getReportApi 接口返回数据格式错误")
                return {'error':"接口返回数据格式错误"}
            if result.get("code", 0) != 1:  # code=0 表示成功
                current_app.logger.error(f"getReportApi 操作失败: {result.get('msg', '未知错误')}")
                return {'error_msg':f"操作失败: {result.get('msg', '未知错误')}"}
            else:
                return {'data':result['data']}

        except requests.exceptions.RequestException as e:
            current_app.logger.error(f"getReportApi 网络请求失败: {e}")
            return {'error':f"网络请求失败: {e}"}
        except ValueError as e:
            current_app.logger.error(f"getReportApi 数据解析失败: {e}")
            return {'error':f"数据解析失败: {e}"}
        except Exception as e:
            current_app.logger.error(f"getReportApi 未知错误: {e}")
            return {'error':f"未知错误: {e}"}